#
# common.py
#
# An include file providing common functions for PDP helper scripts.
# Not intended to be called directly.
#
# Copyright (c) 2019 TOSHIBA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#

import os
import subprocess
import re
import yaml
import apt
import apt_pkg
import json
import sys

PDP_VERSION = "3.2"
DEBIAN_CODE_NAMES = ['jessie', 'stretch', 'buster', 'bullseye']

# Text colors displayed on console
ADD_TXT_COLOR_PRE = "\033[32m"
ADD_TXT_COLOR_POST = "\033[00m"
RMV_TXT_COLOR_PRE = "\033[31m"
RMV_TXT_COLOR_POST = "\033[00m"
INPUT_TXT_COLOR_PRE = "\033[36m "
INPUT_TXT_COLOR_POST = "\033[00m"
IP_Q_SEL_INFO_TXT_COLOR_PRE = "\033[35m "
IP_Q_SEL_INFO_TXT_COLOR_POST = "\033[00m"
IP_ASTERISK_COLOR = "\033[31m"
IP_UNDERLINE_PRE = "\033[4m"
ERROR_TAG = "ERROR: "
INFO_TAG = "INFO: "

# KEY Names for YAML output
PDP_REVISION_KEY = "pdp_revision"
DEPENDS_KEY = "depends"
BIN_PKGS_KEY = "bin_pkgs"
N_CVE_KEY = "n_cve"
SECURITY_CRITERIA_KEY = "security_criteria"
IN_TARGET_KEY = "in_target"
REASON_KEY = "reason"
PROPOSER_KEY = "proposer"
DATE_KEY = "date"
DEBIAN_VER_KEY = "debian_version"
SRC_PKGS_KEY = "src_pkgs"

APT_SOURCE_LIST_DATA = """# Auto Generated file
deb %s %s main
deb-src %s %s main
"""


def evaluate_security_criteria(apt, bin_pkg_list):
    """
    Evaluates security criteria by checking list of security tags available in the debian package tag information.
    Follow the below link for the list of security criteria tags
    https://gitlab.com/cip-project/cip-core/cip-pkglist/-/blob/master/doc/pdp.md#security-criteria
    """
    security_criteria_tags = ['security::', 'network::', 'devel::lang', 'devel::compiler']
    matched_security_criteria_tags = set()
    # Get Debian package tag information for each selected binary package and check if any of the tag
    # information is matched with security criteria tags
    for bin_pkg in bin_pkg_list:
        tag_info_list = apt.apt_get_bin_pkg_tag_info(bin_pkg)
        # Search security_criteria_tag substrings in the tag_info_list of strings
        tags_found = [tag for sec_tag in security_criteria_tags for tag in tag_info_list if sec_tag in tag]
        matched_security_criteria_tags.update(tags_found)
    security_criteria = ', '.join(map(str, sorted(matched_security_criteria_tags)))
    if security_criteria == '':
        security_criteria = 'None'
    return security_criteria


def input_multiline_text(question_str):
    """
    Read User input with multi line text
    :param question_str: question text
    :return: returns user provided text
    """
    print(INPUT_TXT_COLOR_PRE + question_str + "(Press Ctrl-d when complete)" + INPUT_TXT_COLOR_POST)
    res = "".join(sys.stdin.readlines())
    return res.strip()


def input_text(question_str):
    """
    Ask user input until valid text is provided
    :param question_str: question text
    :return: returns user provided text
    """
    while True:
        res = raw_input(INPUT_TXT_COLOR_PRE + question_str + INPUT_TXT_COLOR_POST).strip()
        if not res:
            continue
        else:
            return res


def get_underlined_text(text, start_index=0, end_index=0):
    """Underline the characters from start_index to end_index in the given text"""
    return text[:start_index] + IP_UNDERLINE_PRE + text[start_index:end_index] + INPUT_TXT_COLOR_POST + text[end_index:]


def input_choose_radio2(question_str, options_list):
    """
    Ask user to input the option from the given option list
    :param question_str: prepended question to the user, (options are added to this text)
    :param options_list: options list to the user
    :return: return user selected option string
    """
    # Prepare question string to user with the options given
    options_str = "[" + "/ ".join(list(map(lambda x: get_underlined_text(x, 0, 1), options_list))) + "]: "

    # Prepare possible options dictionary to accept
    pos_opts_map = map(lambda x: dict(map(lambda y: (y, x), [x.lower(), x[0].lower()])), options_list)
    pos_opts_dict = {x: y for map_list in pos_opts_map for x, y in map_list.items()}

    while True:
        # read user input until it is available in possible options
        res = raw_input(INPUT_TXT_COLOR_PRE + question_str + INPUT_TXT_COLOR_POST + options_str).strip().lower()
        if res in pos_opts_dict:
            return pos_opts_dict[res]


def input_choose_bin_pkg(combo_list, pdp_bin_list=[]):
    """
    Ask user to first choose the action for proposed binary packages and then select the binary packages for it.
    :param combo_list: list of to propose
    :param pdp_bin_list: list of binary packages available in pkglist
    :return: tuple(action, selected_bin_pkgs)
    """
    is_present_pdp = False
    combo_dict = dict(map(lambda x, y: (str(x), y), range(1, len(combo_list)+1), combo_list))
    for combo_index in sorted(combo_dict):
        combo_item = combo_dict[combo_index]
        if combo_item in pdp_bin_list:
            # mark with * if the binary package is exist in pkglist
            combo_item = combo_item + IP_ASTERISK_COLOR + "*" + IP_Q_SEL_INFO_TXT_COLOR_POST
            is_present_pdp = True
        print(INPUT_TXT_COLOR_PRE + "\t" + combo_index + ": " + combo_item + INPUT_TXT_COLOR_POST)

    if is_present_pdp:
        print(IP_ASTERISK_COLOR + "\t'*' indicates the binary package is already included in pkglist_<debian version>.yml"
                              " file" + IP_Q_SEL_INFO_TXT_COLOR_POST)

    comb_sel_set = set()
    while True:
        res = raw_input(INPUT_TXT_COLOR_PRE + "Select the packages with comma separated (e.g: 1,3,4): " + INPUT_TXT_COLOR_POST)
        comb_sel = res.split(',')
        # if any of the selection number not in the range, ask user to input correct selection
        if any([sel not in combo_dict for sel in comb_sel]):
            print(INPUT_TXT_COLOR_PRE + "Invalid Selection!!!" + INPUT_TXT_COLOR_POST)
            continue
        else:
            comb_sel_set = set([combo_dict[sel] for sel in comb_sel])
            break

    return list(comb_sel_set)


def input_choose_combo(question_str, combo_list, combo_list_key_start=1):
    """
    Ask user input as combination of numbers for the given option list
    :param question_str: Question text to display
    :param combo_list: list of options to display for user
    :param combo_list_key_start: option numbers start to display for the list
    :return: returns the list of user selected options
    """
    combo_list_keys = range(combo_list_key_start, len(combo_list) + combo_list_key_start)
    print(INPUT_TXT_COLOR_PRE + question_str + INPUT_TXT_COLOR_POST)
    print(INPUT_TXT_COLOR_PRE + "\t" + "\n\t".join(str(i) + ': ' + combo_list[i - combo_list_key_start]
                                                   for i in combo_list_keys) + INPUT_TXT_COLOR_POST)
    comb_sel_set = set()
    while True:
        res = raw_input(INPUT_TXT_COLOR_PRE + "Input the numbers in comma separated (eg: 1,3,4): " + INPUT_TXT_COLOR_POST)
        comb_sel = res.split(',')
        valid = True
        for sel in comb_sel:
            if not sel.isdigit():
                valid = False
                break

            comb_sel_num = int(sel)
            if comb_sel_num not in combo_list_keys:
                valid = False
                break

            comb_sel_set.add(combo_list[comb_sel_num - combo_list_key_start])

        if not valid:
            print(INPUT_TXT_COLOR_PRE + "Invalid Selection!!!" + INPUT_TXT_COLOR_POST)
            continue
        else:
            break

    return list(comb_sel_set)


def input_choose_radio(question_str, radio_list, radio_list_num_start=1, prv_sel_info_dict=dict()):
    """
    Ask user input to choose one of the option in the given list
    :param question_str: Question text to display
    :param radio_list: list of options to choose
    :param radio_list_num_start: list number to start
    :param prv_sel_info_dict: previous selection information
    :return: returns the user selected option
    """
    radio_list_keys = range(radio_list_num_start, len(radio_list) + radio_list_num_start)
    is_asterisk_msg = False
    print(INPUT_TXT_COLOR_PRE + question_str + INPUT_TXT_COLOR_POST)
    for i in range(radio_list_num_start, len(radio_list) + radio_list_num_start):
        list_item = radio_list[i - radio_list_num_start]
        if list_item in prv_sel_info_dict:
            cur_sel, pkglist_sel = prv_sel_info_dict[list_item]
            sel_this_pkg = ""
            if pkglist_sel:
                sel_this_pkg = IP_ASTERISK_COLOR + "*" + IP_Q_SEL_INFO_TXT_COLOR_POST
                is_asterisk_msg = True
            if len(cur_sel):
                sel_this_pkg += IP_Q_SEL_INFO_TXT_COLOR_PRE + \
                    " => You've selected this in pkgs: " +\
                    str(list(cur_sel)) + IP_Q_SEL_INFO_TXT_COLOR_POST

            print(INPUT_TXT_COLOR_PRE + "\t" + str(i) + ": " + list_item + INPUT_TXT_COLOR_POST + sel_this_pkg)
        else:
            print(INPUT_TXT_COLOR_PRE + "\t" + str(i) + ": " + list_item + INPUT_TXT_COLOR_POST)

    if is_asterisk_msg:
        print(IP_ASTERISK_COLOR + "\t'*' indicates the package is included in final pkglist.txt file" + IP_Q_SEL_INFO_TXT_COLOR_POST)

    rad_sel_str = ""
    while True:
        res = raw_input(INPUT_TXT_COLOR_PRE + "Input the number: " + INPUT_TXT_COLOR_POST)
        if res.isdigit():
            rad_sel_num = int(res)
            if rad_sel_num in radio_list_keys:
                rad_sel_str = radio_list[rad_sel_num - radio_list_num_start]
                break
            else:
                continue
        else:
            continue

    return rad_sel_str


def print_nrml_text(text):
    print(" " + text)


def print_add_text(text):
    print(ADD_TXT_COLOR_PRE + "+" + text + ADD_TXT_COLOR_POST)


def print_rmv_text(text):
    print(RMV_TXT_COLOR_PRE + "-" + text + RMV_TXT_COLOR_POST)


def die(text):
    print(ERROR_TAG + text)
    exit(1)


class Apt:
    """
    Apt class provides functions for apt configuration and apt-cache commands
    """

    def __init__(self):
        self.__codename = ""
        self.__apt_dir = os.getcwd() + "/.apt"
        self.__apt_dir_state = self.__apt_dir + "/state"
        self.__apt_dir_lists = self.__apt_dir_state + "/lists"
        self.__apt_dir_status = self.__apt_dir_state + "/status"
        self.__apt_dir_cache = self.__apt_dir + "/cache"
        self.__apt_source_list_file = self.__apt_dir + "/sources.list"
        self.__src_info_file = self.__apt_dir + "/source_info.json"
        self. __apt_mirror = "http://deb.debian.org/debian"
        self.__cache = dict()
        self.__src_info_dict = dict()

    def apt_cache_get_src_info(self, pkg_name):
        """
        provides the source package information for the given package name
        :param pkg_name: source package name
        :return: returns tuple of source package name, source package version, source package binary package list
        """
        cache = self.__cache
        src_pkg_name = ""
        src_pkg_ver = ""
        src_pkg_bin_list = []
        if pkg_name not in cache:
            src_pkg_name = pkg_name
        else:
            src_pkg_name = cache[pkg_name].candidate.source_name

        if src_pkg_name in self.__src_info_dict:
            src_pkg_bin_list = [str(bp) for bp in self.__src_info_dict[src_pkg_name]]
            src_pkg_ver = cache[src_pkg_bin_list[0]].candidate.source_version
        else:
            src_pkg_name = ""
            src_pkg_ver = ""

        return src_pkg_name, src_pkg_ver, src_pkg_bin_list

    def get_validated_dependency_pkg_name(self, dep_pkg):
        """
        Validate the dependency package name against the following conditions:
            - package version condition
            - availability of the dependency package in the current distribution
        :param dep_pkg: dependency package of type: apt::Package
        :return: return the validated package name
        """
        c = self.__cache
        dep_pkg_name = dep_pkg.name
        if dep_pkg_name in c:
            if dep_pkg.relation_deb and not apt_pkg.check_dep(c[dep_pkg.name].candidate.version,
                                                              dep_pkg.relation_deb, dep_pkg.version):
                # if dependency version condition is not satisfied, add the remark as 'not satisfied'
                dep_pkg_name = dep_pkg.name + " *(" + dep_pkg.relation_deb + dep_pkg.version + \
                    ") version condition is not satisfied with current distribution package" \
                    " version " + c[dep_pkg.name].candidate.version
        elif c.is_virtual_package(dep_pkg.name):
            # if dependency is virtual package, mark it as virtual
            dep_pkg_name = '<' + dep_pkg.name + '>'
        else:
            # if dependency package is not present in the cache, mark it as 'package not available'
            dep_pkg_name = dep_pkg.name + " *package not available in the current distribution"
        return dep_pkg_name

    def is_virtual_package(self, pkg_name):
        """
        check the package is virtual
        :param pkg_name:
        :return: True if the package is virtual else False
        """
        vir_pkg_name = pkg_name
        if vir_pkg_name.startswith('<'):
            vir_pkg_name = pkg_name[1:pkg_name.find('>')]
        return self.__cache.is_virtual_package(vir_pkg_name)

    def apt_cache_get_depends_list(self, pkg_name):
        """
        provides the dependency list and virtual package providers for the given binary package name
        dependency list is double list contains as list of 'or' list, if dependency pkg doesn't have
        'or' list then it contains list of single element
        :param pkg_name: binary package name
        :return: returns tuple of dependency list, virtual packages providers dictionary
        """
        c = self.__cache
        dep_set = set()
        vir_dict = dict()
        if pkg_name in c:
            dependencies = c[pkg_name].candidate.dependencies
            for dependency_list in dependencies:
                or_dep_set = set()
                for or_dep in dependency_list:
                    or_pkg_name = self.get_validated_dependency_pkg_name(or_dep)
                    if self.is_virtual_package(or_pkg_name):
                        # if dependency is virtual package, add its providers list to a dictionary
                        vir_dict[or_pkg_name] = tuple(
                            sorted([provider.name for provider in c.get_providing_packages(or_dep.name)]))
                    or_dep_set.add(or_pkg_name)
                dep_set.add(tuple(sorted(or_dep_set)))
        return dep_set, vir_dict

    def apt_get_bin_pkg_list(self):
        """
        read the binary packages from the apt cache downloaded file
        :return:
        """
        return self.__cache.keys()

    def apt_get_bin_pkg_tag_info(self, bin_pkg_name):
        """
        Gives Debian package tag information for the given binary package
        eg: Tag information for 'passwd' package
            admin::user-management, implemented-in::c, interface::commandline, role::program, security::authentication
        :param bin_pkg_name:
        :return: list of tags(str) available for Debian package
        """
        c = self.__cache
        pkg_tags = list()
        if bin_pkg_name in c:
            tag_info = c[bin_pkg_name].candidate.record.get('Tag')
            if tag_info:
                pkg_tags = map(str.strip, tag_info.split(','))
        return pkg_tags

    def apt_initialize(self, codename):
        """
        Initializes the apt for the given Debian codename
        :param codename: Debian version name
        :return: True if initialization is success
        """
        try:
            update = False
            self.__codename = codename
            if not os.path.exists(self.__apt_dir):
                # initialize the apt directory if not exist
                os.mkdir(self.__apt_dir)
                os.mkdir(self.__apt_dir_state)
                with open(self.__apt_dir_status, 'w+') as f:
                    os.utime(self.__apt_dir_status, None)
                os.mkdir(self.__apt_dir_cache)
                os.mkdir(self.__apt_dir_lists)
                update = True

            if os.path.exists(self.__apt_source_list_file):
                # check if the source list data contains with the same code name or update
                data = APT_SOURCE_LIST_DATA % (self.__apt_mirror, self.__codename, self.__apt_mirror, self.__codename)
                with open(self.__apt_source_list_file, "r") as fr:
                    if data not in fr.read():
                        update = True

            if update:
                # save the source list data for the given code name to the source.list
                data = APT_SOURCE_LIST_DATA % (self.__apt_mirror, self.__codename, self.__apt_mirror, self.__codename)
                with open(self.__apt_source_list_file, "w") as writer:
                    writer.write(data)

            apt_pkg.init_config()
            apt_pkg.config.set("Dir::Etc::sourcelist", self.__apt_source_list_file)
            apt_pkg.config.set("Dir::Etc::sourceparts", "-")
            apt_pkg.config.set("Dir::State", self.__apt_dir_state)
            apt_pkg.config.set("Dir::State::Status", self.__apt_dir_status)
            apt_pkg.config.set("Dir::Cache", self.__apt_dir_cache)
            apt_pkg.config.set("APT::Architectures::", "")
            apt_pkg.config.set("Debug::NoLocking", "true")
            apt_pkg.init_system()
            self.__cache = apt.Cache()
            if update:
                print("apt configuring to %s ..." % self.__codename)
                self.__cache.update()
            self.__cache.open()

            if not os.path.exists(self.__src_info_file):
                # Save the Source package names and its binaries to a file
                src_info_dict = dict()
                for pkg in self.__cache:
                    src_pkg_name = self.__cache[pkg].candidate.source_name
                    if src_pkg_name in src_info_dict:
                        src_info_dict[src_pkg_name].append(pkg)
                    else:
                        src_info_dict[src_pkg_name] = [pkg]

                with open(self.__src_info_file, 'w') as src_info_file:
                    json.dump(src_info_dict, src_info_file)
                self.__src_info_dict = src_info_dict
            else:
                with open(self.__src_info_file, "r") as f:
                    self.__src_info_dict = json.load(f)

        except IOError as err:
            print("apt initialize failed: " + err.strerror)
            return False
        return True


class PDPInfo:
    """
    This class provide functions to fetch information from file
    """

    class BinPkgData:
        """
        Data structure to store individual binary package details
        """

        def __init__(self):
            self.bin_pkg_pdp_state = ""
            self.bin_pkg_depends = list()  # list of dependencies

    class SrcPkgData:
        """
        Data structure for storing source package data
        """

        def __init__(self):
            self.src_pkg_name = "-"
            self.proposer = list()
            self.pdp_version = "-"
            self.in_target = "-"
            self.security_criteria = "-"
            self.n_cve = "-"
            self.reason = "-"
            self.bin_pkg_data_dict = dict()  # dictionary of binary package name(key) and binary package data BinPkgData(value)

    def __init__(self, codename):
        self.__is_load_success = False
        self.__pdp_info_dict = dict()  # dictionary of source package name(key) and source package data SrcPkgData(value)
        self.codename = codename
        self.__pkglist = "pkglist_" + codename + ".yml"
        pass

    def load_pdp(self):
        """
        open pdp file for the code name and load pdp data in to local structure
        :return: True if load is success
        """
        try:
            self.__pdp_info_dict = dict()
            with open(self.__pkglist, 'r') as pdp_info:
                data = yaml.load(pdp_info, Loader=yaml.SafeLoader)

                pdp_pkg_data_dict = dict()
                for src_pkg, src_pkg_info in data.items():
                    pdp_pkg_data = PDPInfo.SrcPkgData()
                    pdp_pkg_data.src_pkg_name = src_pkg
                    pdp_pkg_data.proposer = src_pkg_info[PROPOSER_KEY]
                    if not isinstance(pdp_pkg_data.proposer, list):
                        pdp_pkg_data.proposer = [pdp_pkg_data.proposer]
                    pdp_pkg_data.pdp_version = src_pkg_info[PDP_REVISION_KEY]
                    pdp_pkg_data.in_target = src_pkg_info[IN_TARGET_KEY]
                    pdp_pkg_data.security_criteria = src_pkg_info[SECURITY_CRITERIA_KEY]
                    pdp_pkg_data.n_cve = src_pkg_info[N_CVE_KEY]
                    pdp_pkg_data.reason = src_pkg_info[REASON_KEY]

                    bin_pkg_data_dict = dict()
                    bin_pkg_dict = src_pkg_info[BIN_PKGS_KEY]
                    for bp, bp_info in bin_pkg_dict.items():
                        pdp_bin_pkg_data = PDPInfo.BinPkgData()
                        pdp_bin_pkg_data.bin_pkg_depends = bp_info[DEPENDS_KEY]
                        bin_pkg_data_dict[bp] = pdp_bin_pkg_data

                    pdp_pkg_data.bin_pkg_data_dict = bin_pkg_data_dict
                    pdp_pkg_data_dict[src_pkg] = pdp_pkg_data
                self.__pdp_info_dict = pdp_pkg_data_dict
                self.__is_load_success = True
        except IOError as err:
            print("Failed to load " + self.__pkglist + ", Error: " + err.strerror)
        return self.__is_load_success

    def dump_pdp(self):
        """
        save the pdp data structure to the file
        :return: None
        """
        data = dict()
        for sp, sp_info in self.__pdp_info_dict.items():
            sp_info_dict = dict()
            bp_list_dict = dict()
            for bp_name, bp_data in sp_info.bin_pkg_data_dict.items():
                bp_list_dict[bp_name] = {DEPENDS_KEY: bp_data.bin_pkg_depends}

            sp_info_dict[BIN_PKGS_KEY] = bp_list_dict
            sp_info_dict[PROPOSER_KEY] = sp_info.proposer
            sp_info_dict[PDP_REVISION_KEY] = sp_info.pdp_version
            sp_info_dict[IN_TARGET_KEY] = sp_info.in_target
            sp_info_dict[SECURITY_CRITERIA_KEY] = sp_info.security_criteria
            sp_info_dict[N_CVE_KEY] = sp_info.n_cve
            sp_info_dict[REASON_KEY] = sp_info.reason

            data[sp] = sp_info_dict

        with open(self.__pkglist, 'w') as pdp_info:
            yaml.dump(data, pdp_info, default_flow_style=False, )
            print("saved to file " + self.__pkglist)

    def get_bin_pkg_info(self, bin_pkg_name):
        """
        Search in PDP data structure and provide the binary package data along with source package name and its version
        :param bin_pkg_name: binary package name
        :return: SrcPkgData structure contains binary package data if exist or None
        """

        data = self.__pdp_info_dict
        pdp_pkg_data = PDPInfo.SrcPkgData()
        for sp, sp_info in data.items():
            if bin_pkg_name in sp_info.bin_pkg_data_dict:
                pdp_pkg_data.src_pkg_name = sp
                pdp_pkg_data.proposer = sp_info.proposer
                pdp_pkg_data.pdp_version = sp_info.pdp_version
                pdp_pkg_data.in_target = sp_info.in_target
                pdp_pkg_data.security_criteria = sp_info.security_criteria
                pdp_pkg_data.n_cve = sp_info.n_cve
                pdp_pkg_data.reason = sp_info.reason
                pdp_pkg_data.bin_pkg_data_dict = {bin_pkg_name: sp_info.bin_pkg_data_dict[bin_pkg_name]}
        return pdp_pkg_data

    def get_src_pkg_info(self, src_pkg_name):
        """
        Search in PDP data structure and provide the source package details including binaray packages
        :param src_pkg_name: source package name
        :return: SrcPkgData structure contains source package data if exist or None
        """
        data = self.__pdp_info_dict
        if src_pkg_name in data:
            return data[src_pkg_name]

    def set_src_pkg_info(self, src_pkg_data):
        """
        Save the binary pkg data in to the PDP data structure
        :param src_pkg_data:
        :return:
        """
        self.__pdp_info_dict[src_pkg_data.src_pkg_name] = src_pkg_data

    def update_src_pkg_info(self, src_pkg_data):
        """
        update individual binary information in side the source package information
        :param src_pkg_data:
        :return: None
        """
        data = self.__pdp_info_dict
        if src_pkg_data.src_pkg_name in data:
            for bp, bp_info in src_pkg_data.bin_pkg_data_dict.items():
                data[src_pkg_data.src_pkg_name].bin_pkg_data_dict[bp] = bp_info
        else:
            data[src_pkg_data.src_pkg_name] = src_pkg_data

    def is_bin_pkg_exist(self, bin_pkg_name):
        """
        checks if binary package is present in PDP info file and return True if exist
        :param bin_pkg_name:
        :return: True if binary package exist, else False
        """
        data = self.__pdp_info_dict
        for sp, sp_info in data.items():
            if bin_pkg_name in sp_info.bin_pkg_data_dict:
                return True
        return False

    def is_src_pkg_exist(self, src_pkg_name):
        """
        Checks if source package is present in the PDP info file and return True if exist
        :param src_pkg_name:
        :return: True if source package exist, else False
        """
        data = self.__pdp_info_dict
        if src_pkg_name in data:
            return True
        return False

    def remove_bin_pkg(self, bin_pkg_name):
        """
        Removes the given binary package information from PDP file
        :param bin_pkg_name:
        :return: True if successfully removed, else False
        """
        data = self.__pdp_info_dict
        for sp, sp_info in data.items():
            if bin_pkg_name in sp_info.bin_pkg_data_dict:
                del self.__pdp_info_dict[sp].bin_pkg_data_dict[bin_pkg_name]
                return True
        return False

    def remove_src_pkg(self, src_pkg_name):
        """
        Removes the given source package information from PDP file
        :param src_pkg_name:
        :return: True if successfully removed, else False
        """
        data = self.__pdp_info_dict
        if src_pkg_name in data:
            del self.__pdp_info_dict[src_pkg_name]
            return True
        return False

    def get_pdp_info(self):
        """
        Return the pdp data
        :return: dictionary of SrcPkgData
        """
        return self.__pdp_info_dict


class PDPProposal:
    """
    PDPProposal class provides functions to save the package proposal information to the file (in YAML) and also read the
    request information from the file and keep in data structure.
    """

    class SrcPkgInfo:
        """
        Data structure to store the source package information
        """

        def __init__(self):
            self.bin_pkg_dict = dict()  # dictionary of binary package name(key) and its dependency list (value)
            self.in_target = "False"
            self.security_criteria = "-"
            self.n_cve = "-"
            self.reason = ""

    class ProposalInfo:
        """
        Data structure to store proposal information
        """

        def __init__(self):
            self.proposer_name = ""
            self.proposal_date = ""
            self.pdp_revision = PDP_VERSION
            self.proposed_src_pkgs = dict()  # dictionary of source package name(key) and its information SrcPkgInfo (value)
            self.proposed_debian_version = ""

    def __init__(self):
        pass

    def load(self, file_name):
        """
        Load the Request information from the given file and place it in data structure
        :param file_name: file name that contains the Request information
        :return: ProposalInfo data structure
        """
        try:
            with open(file_name, 'r') as pdp_info:
                data = yaml.load(pdp_info, Loader=yaml.SafeLoader)
                if data[PDP_REVISION_KEY] != PDP_VERSION:
                    print("Loaded proposal file has different PDP Revision from expected: " + PDP_VERSION)
                    return
                return self.dict_to_proposal_info(data)
        except IOError as err:
            print("Load Request info file failed " + file_name + " " + err.strerror)
        return

    def save(self, request_info, file_name):
        """
        Save the given Request information data structure in to given file
        :param request_info: request information data structure
        :param file_name: file name to save the request information
        :return: True if save is success, otherwise False
        """
        if not request_info or not file_name:
            return False

        res = ""
        with open(file_name, 'w') as pdp_info:
            res = yaml.dump(self.proposal_info_to_dict(request_info), pdp_info, default_flow_style=False, )

        if not res:
            return False
        else:
            return True

    def print_req_info(self, proposal_info):
        """
        print filled request info data on console in YAML format
        :param proposal_info:
        :return:
        """
        print(yaml.dump(self.proposal_info_to_dict(proposal_info)))

    def proposal_info_to_dict(self, proposal_info):
        """
        Converts local data structure to dictionary
        :param proposal_info:
        :return:
        """
        proposal_info_dict = dict()
        proposal_info_dict[PDP_REVISION_KEY] = proposal_info.pdp_revision
        proposal_info_dict[PROPOSER_KEY] = proposal_info.proposer_name
        proposal_info_dict[DATE_KEY] = proposal_info.proposal_date
        proposal_info_dict[DEBIAN_VER_KEY] = proposal_info.proposed_debian_version
        src_pkgs = dict()
        for sp, src_pkg_info in proposal_info.proposed_src_pkgs.items():
            bp_dict = dict()
            for bp, dp_list in src_pkg_info.bin_pkg_dict.items():
                bp_dict[bp] = {DEPENDS_KEY: dp_list}

            src_pkgs[sp] = {BIN_PKGS_KEY: bp_dict,
                            IN_TARGET_KEY: src_pkg_info.in_target,
                            SECURITY_CRITERIA_KEY: src_pkg_info.security_criteria,
                            N_CVE_KEY: src_pkg_info.n_cve,
                            REASON_KEY: src_pkg_info.reason}
        proposal_info_dict[SRC_PKGS_KEY] = src_pkgs
        return proposal_info_dict

    def dict_to_proposal_info(self, proposal_info_dict):
        """
        Converts dictionary to local data structure
        :param proposal_info_dict:
        :return:
        """
        proposal_info = PDPProposal.ProposalInfo()
        proposal_info.pdp_revision = proposal_info_dict[PDP_REVISION_KEY]
        proposal_info.proposer_name = proposal_info_dict[PROPOSER_KEY]
        proposal_info.proposal_date = proposal_info_dict[DATE_KEY]
        proposal_info.proposed_debian_version = proposal_info_dict[DEBIAN_VER_KEY]
        proposal_info.proposed_src_pkgs = proposal_info_dict[SRC_PKGS_KEY]
        for sp, sp_info in proposal_info.proposed_src_pkgs.items():
            src_pkg_info = PDPProposal.SrcPkgInfo()
            src_pkg_info.n_cve = sp_info[N_CVE_KEY]
            src_pkg_info.security_criteria = sp_info[SECURITY_CRITERIA_KEY]
            src_pkg_info.in_target = sp_info[IN_TARGET_KEY]
            src_pkg_info.reason = sp_info[REASON_KEY]
            bp_dict = dict()
            for bp, bp_info in sp_info[BIN_PKGS_KEY].items():
                bp_dict[bp] = bp_info[DEPENDS_KEY]

            src_pkg_info.bin_pkg_dict = bp_dict
            proposal_info.proposed_src_pkgs[sp] = src_pkg_info
        return proposal_info
